試問: @click.prevent.self
和 @click.self.prevent
的差別是什麼?
...
回答不出來的人,還有想即時打開 Vue 文件找答案的人,可以看看這篇XD。
Vue 在 Event Handling 這個章節中提到,v-on 的修飾符可以串接,但要注意先後順序,會影響事件觸發的條件。
原文如下:
Order matters when using modifiers because the relevant code is generated in the same order. Therefore using @click.prevent.self will prevent click's default action on the element itself and its children, while @click.self.prevent will only prevent click's default action on the element itself.
裡面舉了個例子:
@click.prevent.self
:會預防元素和他的後代在 click 事件下的預設行為@click.self.prevent
:只會預防元素本身在 click 事件下的預設行為Okay...
聽起來很有道理,腦補一下是蠻合理的,對吧?
然後下次遇到的時又會想不起來,也就是說,根本不知道為什麼是這樣哈哈哈(自我反省時間XD)
所以,今天的目標是:從 DOM 事件傳遞和原始碼了解 v-on 修飾符串接,進而理解 @click.prevent.self
和 @click.self.prevent
的差異。
event.preventDefault()
在理解 @click.prevent.self
和 @click.self.prevent
的差異之前,需要先了解 event.preventDefault()
!
請看下面的範例或 Codepen,如果你已經知道,為什麼在這裡點擊 <a>
連結並不會跳轉,就可以略過這段,直接到下一段看 vuejs 處理 v-on 修飾符時的相關原始碼。
<div>
<a href="https://vuejs.org/">Vue 官網</a>
</div>
const outer = document.querySelector("div");
const inner = document.querySelector("a");
outer.addEventListener("click", function (event) {
console.log("執行 outer handler")
event.preventDefault();
});
inner.addEventListener("click", function (event) {
console.log("執行 inner handler")
});
使用者在瀏覽器觸發事件,會開始一連串的事件傳遞,事件傳遞過程,會經過各個 DOM 元素上的 handler,瀏覽器會:
使用者操作網頁時會觸發各種事件,有些事件有預設的動作,舉例來說:
<a>
連結會導向他帶有的 URL<form>
表單的 submit 按鈕,會向伺服器提交表單(最常見的使用情境就是 1 跟 2!)
event.preventDefault()
相信大部分人都很熟悉,在綁定事件監聽器時,綁定的 listener / handler 函式可以拿到一個 event
物件,讓我們去取用相關屬性或方法,包含:呼叫 event.preventDefault()
來取消預設行為。
每次觸發事件的時候,event
物件就會經歷 DOM 事件傳遞的機制 ---- 捕獲與冒泡,這個 event
物件會被傳進一路上遇到的 handler 中。
比較容易忽略的地方是,這個 event
物件是同一個,而且在過程中,一旦有 handler 呼叫 event.preventDefault()
,event
物件下的 defaultPrevented
屬性就會被標記為 true
,告訴瀏覽器,要取消這次事件的預設行為。
Calling preventDefault() stops all related default actions of an event object. --W3C
這也是為什麼在剛剛的範例中,雖然 <a>
元素的 handler 上並沒有呼叫 event.preventDefault()
,最後卻不會跳轉的原因。
小結
evnet
物件都是同一個handler
內呼叫 event.preventDefault()
後,event.defaultPrevented
會被標記為 true
event.preventDefault()
可能會在非(開發者)本意下,不小心影響其他 DOM 元素響應預設行為modifierGuards
為物件,key
為修飾符,value
為函式,用來回傳判別或呼叫對應方法,後續文章中,我會稱他們為 guard function,可以利用修飾符名稱作為 key
,從 modifierGuards
取得對照的 guard function。
const systemModifiers = ['ctrl', 'shift', 'alt', 'meta']
type KeyedEvent = KeyboardEvent | MouseEvent | TouchEvent
const modifierGuards: Record<
string,
(e: Event, modifiers: string[]) => void | boolean
> = {
stop: e => e.stopPropagation(), //呼叫 event 停止冒泡的 method
prevent: e => e.preventDefault(), //呼叫 event 取消預設行為的 method
self: e => e.target !== e.currentTarget,
ctrl: e => !(e as KeyedEvent).ctrlKey,
shift: e => !(e as KeyedEvent).shiftKey,
alt: e => !(e as KeyedEvent).altKey,
meta: e => !(e as KeyedEvent).metaKey,
left: e => 'button' in e && (e as MouseEvent).button !== 0,
middle: e => 'button' in e && (e as MouseEvent).button !== 1,
right: e => 'button' in e && (e as MouseEvent).button !== 2,
exact: (e, modifiers) =>
systemModifiers.some(m => (e as any)[`${m}Key`] && !modifiers.includes(m))
}
.stop
v.s .prevent
:
這兩個修飾符比較特別,會呼叫對應的 method,回傳 undefined
。
stop: e => e.stopPropagation(),
呼叫 event 停止冒泡的 methodprevent: e => e.preventDefault(),
呼叫 event 取消預設行為的 method,將 event.defaultPrevented
標示為 true
,表示取消這次事件的預設行為。.self
:self: e => e.target !== e.currentTarget,
target
是否為 currentTarget
target
) 是否為事件監聽者(currentTarget
)false
,兩者不同則回傳 true
註:這邊會覺得像反反邏輯,和 Vue 後面的設計有關。
可以想成 guard function 是要用來擋住「不要的情況」,所以設定上,會在狀況和修飾符不符合的時候,回傳 true
,表示 guard 出動、阻擋繼續執行 handler。
.ctrl
修飾符,觸發事件時,如果 ctrl 在按下狀態,會回傳 false
。ctrl: e => !(e as KeyedEvent).ctrlKey,
shift: e => !(e as KeyedEvent).shiftKey,
alt: e => !(e as KeyedEvent).altKey,
meta: e => !(e as KeyedEvent).metaKey,
註:瀏覽器的 KeyboardEvent
、MouseEvent
和 TouchEvent
物件下,有 ctrlKey
、shiftKey
、altKey
、metaKey
這 4 個屬性,事件觸發當下,系統鍵處於按下狀態則為 true
,非按下狀態則為 false
,即使監聽的是 keyup 事件,也是一樣的。
button
屬性,並且數字是否「沒有對應」到修飾符。left: e => 'button' in e && (e as MouseEvent).button !== 0,
middle: e => 'button' in e && (e as MouseEvent).button !== 1,
right: e => 'button' in e && (e as MouseEvent).button !== 2,
.exact
const systemModifiers = ['ctrl', 'shift', 'alt', 'meta']
//中略
exact: (e, modifiers) =>
systemModifiers.some(m => (e as any)[`${m}Key`] && !modifiers.includes(m))
}
在 systemModifiers
中,是有否修飾符同時具備以下兩個條件:
有這種情況會回傳 true
,表示不得觸發這次的 handler。
在 v-on 使用修飾符時,Vue 會呼叫 withModifier
來處理。
//傳入的 fn 為 ($event) => customHandler(param1, param2,...)
//傳入的 modifiers 為陣列,如["prevent", "self"]
export const withModifiers = (fn: Function, modifiers: string[]) => {
return (event: Event, ...args: unknown[]) => {
for (let i = 0; i < modifiers.length; i++) {
const guard = modifierGuards[modifiers[i]]
if (guard && guard(event, modifiers)) return
//這裡的 return 會結束函式的執行,符合條件代表不會繼續往下觸發 customHandler
}
return fn(event, ...args)
//回傳並執行 fn,即 customHandler
}
}
這裡先說結論,withModifier
會按照修飾符串接的順序,依序判別並呼叫 guard function,修飾符判別都通過後,再執行 handler 函式內容。
對一般判斷性質的修飾符來說,順序其實不太重要,但是對 .prevent
修飾符來說,順序大有影響,因為他並不是單純判斷事件觸發的環境,而是直接呼叫方法,去改動這次事件物件下的屬性。
如果修飾符中用到 .prevent
,並且放在最前面,執行過程就一定會先呼叫 event.preventDefault()
,無論後面判別是否通過,event
物件的 defaultPrevented
屬性已經被標示為 false
,就算沒有觸發 handler,瀏覽器也已經判斷要取消這次的預設行為了。
如果對程式碼部份有疑惑的人,可以繼續往下看。
直接從範例來看:
<div @click.prevent.self="logMsg('最外層', $event)">
<a href="https://vuejs.org/">Vue</a>
<div>
編譯後會將 handler function(logMsg
) 和裝有修飾符的字串陣列傳進 withModifiers
,像這樣:
withModifiers(($event) => logMsg("\u6700\u5916\u5C64"), ["prevent", "self"])
之後會根據修飾符陣列的長度跑 for 迴圈,遍歷修飾符陣列,利用修飾符名稱作為 key
,從 modifierGuards
去取得對照的 guard function。
for (let i = 0; i < modifiers.length; i++) {
const guard = modifierGuards[modifiers[i]]
if (guard && guard(event, modifiers)) return
}
以剛剛範例來說明,收到的 modifiers
為 ["prevent", "self"]
:
第一圈會處理 prevent
:
"prevent"
對應的 guard function (guard
為 true
)event.preventDefault()
,將 event.defaultPrevented
標示為 true
,表示取消事件預設行為
prevent: e => e.preventDefault(),
prevent
的 guard function 單純呼叫方法,回傳 undifined
,(guard(event, modifiers)
為 false
)self
第二圈會處理 self
"self"
對應的 guard function (guard
為 true
)self: e => e.target !== e.currentTarget,
裡面會判斷並回傳監聽元素與觸發元素是否「不相等」
guard(event, modifiers)
)
true
,withModifiers
整個函式直接被 return
false
,沒有下一個迴圈要執行,所以結束迴圈,繼續往下執行 return fn(event, ...args)
,表示直接執行 handler ($event) => logMsg("\u6700\u5916\u5C64")
一定要釐清,這裡的判別結果只影響「會不會執行這個 handler」!
不管判別結果是什麼,在上一個迴圈,已經將 event.defaultPrevented
標示為 true
,所以,如果這次的事件是從後代 <a>
元素冒泡上來的,最後不會進行頁面跳轉。
剛剛上面的範例是 @click.prevent.self
。
至於 @click.self.prevent
,白話來說,會先判別 self
的條件(監聽元素是否為觸發元素),條件成立的話,才會在下一個迴圈呼叫 event.preventDefault
來取消預設行為,在監聽器註冊在冒泡階段的情況下,也就不會影響到後代元素觸發事件時的預設行為。
簡單總結@click.prevent.self
:先取消預設行為,再判斷觸發元素是不是自己,是的話才執行 handle function,但無論執行與否,取消預設行為已經完成。@click.self.prevent
:只有在確定觸發元素是自己之後,才取消預設行為,接著執行 handle function。
可以到 Vue v-on 範例 去玩玩看兩種順序的差異~